sartUP — UI/UX Spec (Contenitore unico **Admin**, Login esterno, Menu dinamico)
sartUP — UI/UX Spec (Contenitore unico Admin, Login esterno, Menu dinamico)
> Decisione: un solo contenitore Admin/ per tutta l’area autenticata. Il menù è gestito da DB e filtrato per ruolo.
> Il menù di servizio (solo super-admin) ha come prima voce: Configurazione menù.
> Login esterno all’applicazione con layout 1/3–2/3.
---
0) Struttura di riferimento (menu → cartelle)
L’architettura del codice ricalca la gerarchia del menù:- Topbar (L1): macro–sezioni (es. Dashboard, Industria 4.0, Servizio).
- Sidebar (L2/L3/L4): sottosezioni figlie della voce L1 attiva.
- Cartelle parlanti: controllers/views organizzati come il menù.
- Percorso: /login
(guest). - Layout dedicato: resources/views/layouts/auth.blade.php
(nessuna topbar/sidebar). - Struttura 1/3–2/3: - Sinistra (1/3): logo grande (circolare consigliato), nome, descrizione, immagine. - Destra (2/3): form credenziali + “remember me” + “forgot password”.
- Sinistra: logo piccolo circolare + menù principale orizzontale (Livello 1).
- Destra: profilo utente (avatar iniziale + dropdown: “Cambia ruolo”, “Profilo”, “Logout”).
- Due sezioni: - Sidebar sinistra: menù fino a 3 livelli (L2/L3/L4) in base alla voce L1 attiva. - Corpo centrale: 1) Barra breadcrumb + Titolo modulo (coerenti con il ramo corrente del menù). 2) Body del modulo (contenuto della view).
- Login esterno con layout 1/3–2/3, link “forgot”.
- Dopo login: redirect a Admin con Topbar (logo+L1 a sinistra, profilo a destra).
- Sidebar con 3 livelli coerenti con la voce L1 attiva.
- Breadcrumb + Titolo modulo in header del corpo centrale.
- Servizio (super-admin) presente; Configurazione menù come prima voce (CRUD operativo).
- I4.0 → Report → Macchine → Elenco macchine collegate presente (placeholder).
- Il breadcrumb deriva dal ramo del menù (no hardcode).
- Il filtro RBAC si applica a Topbar e, opzionalmente, alla Sidebar.
- Logo consigliato: quadrato (32–40px), reso circolare via CSS.
- Il layout è Blade-first (semplice da personalizzare), pronto a Livewire/Inertia in futuro.
``
app/Http/Controllers/Admin/
Dashboard/DashboardController.php
I40/HomeController.php
I40/Machines/MachinesController.php
Systems/ # “Servizio” (solo super-admin)
Menu/MenuController.php
Users/UsersController.php (placeholder)
Roles/RolesController.php (placeholder)
app/Services/MenuService.php app/Http/Middleware/EnsureActiveRole.php app/Policies/MenuPolicy.php (opzionale) app/Models/Menu.php app/Models/MenuItem.php
resources/views/layouts/admin.blade.php resources/views/layouts/auth.blade.php # login esterno (1/3–2/3) resources/views/auth/{login,select-role,forgot,reset}.blade.php
resources/views/admin/dashboard/index.blade.php resources/views/admin/i40/home.blade.php resources/views/admin/i40/machines/connected.blade.php resources/views/admin/systems/index.blade.php resources/views/admin/systems/menu/{index,create,edit}.blade.php
routes/auth.php
routes/admin.php
`
Nota ruoli: l’area Admin/ è per tutti i ruoli autenticati (operator, maintenance, admin, super-admin). Le voci/azioni sono limitate da RBAC.
“Servizio” è visibile solo al ruolo super-admin e contiene la Configurazione menù (CRUD).
---
1) Login esterno (pagina pubblica)
1.1 Layout Blade (auth)
`blade
{{-- resources/views/layouts/auth.blade.php --}}
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>@yield('title','Accedi | sartUP')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<div class="mx-auto max-w-6xl min-h-screen grid grid-cols-3">
{{-- Colonna sinistra 1/3: branding --}}
<aside class="col-span-1 bg-white border-r p-8 flex flex-col justify-center">
<div class="flex items-center gap-3 mb-6">
<img src="/images/logo.png" alt="sartUP" class="h-12 w-12 rounded-full object-cover">
<div>
<h1 class="text-xl font-semibold">sartUP</h1>
<p class="text-sm text-gray-500">Gestionale modulare per confezione</p>
</div>
</div>
<div class="text-gray-600 text-sm leading-relaxed">
<p>Accedi per gestire modelli, tempi & metodi, integrazioni Industria 4.0 e reportistica.</p>
</div>
{{-- <img src="/images/login-side.jpg" class="mt-8 rounded-xl shadow" alt=""> --}}
</aside> {{-- Colonna destra 2/3: form --}}
<main class="col-span-2 flex items-center justify-center p-8">
<div class="w-full max-w-md bg-white rounded-xl shadow p-6">
@yield('content')
</div>
</main>
</div>
</body>
</html>
`1.2 View Login (form + forgot)
`blade
{{-- resources/views/auth/login.blade.php --}}
@extends('layouts.auth')
@section('title','Accedi')
@section('content')
<h2 class="text-lg font-semibold mb-4">Accedi</h2>
<form method="POST" action="{{ route('login.post') }}" class="space-y-4">
@csrf
<div>
<label class="block text-sm mb-1">Email</label>
<input type="email" name="email" value="{{ old('email') }}" required autofocus class="w-full border rounded p-2">
@error('email') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm mb-1">Password</label>
<input type="password" name="password" required class="w-full border rounded p-2">
@error('password') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex items-center justify-between">
<label class="inline-flex items-center gap-2 text-sm">
<input type="checkbox" name="remember" class="rounded"> Ricordami
</label>
<a class="text-sm underline" href="{{ route('password.request') }}">Hai dimenticato la password?</a>
</div>
<button class="w-full py-2 rounded bg-blue-600 text-white font-medium">Entra</button>
</form>
@endsection
`> Dopo l’autenticazione: redirect all’Admin template (
/admin). Se l’utente ha >1 ruolo → pagina select-role.---
2) Admin template: Topbar, Sidebar 3 livelli, Breadcrumb + Titolo + Body
2.1 Topbar (richieste)
2.2 Body (richieste)
2.3 Layout Blade (admin)
`blade
{{-- resources/views/layouts/admin.blade.php --}}
@php
/ @var \App\Services\MenuService $ms */
$ms = app(\App\Services\MenuService::class);
$menuL1 = $ms->forUserMenu('admin_main', auth()->user()); // topbar
$active = $ms->currentBranchByRoute('admin_main'); // item corrente + ancestors
$sidebarTree = $ms->childrenForTopLevel($active['top'] ?? $menuL1[0] ?? null); // L2/L3/L4
@endphp
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
{{-- TOPBAR --}}
<header class="h-14 shadow flex items-center px-4 bg-white">
<div class="flex items-center gap-3 mr-6">
<img src="/images/logo.png" class="h-8 w-8 rounded-full object-cover" alt="logo">
</div>
<nav class="flex gap-4">
@foreach($menuL1 as $item)
<a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}"
class="font-medium {{ request()->routeIs(($item['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}">
{{ $item['label'] }}
</a>
@endforeach
@role('super-admin')
<a href="{{ route('admin.systems.menu.index') }}"
class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}">Servizio</a>
@endrole
</nav>
<div class="ml-auto flex items-center gap-3">
@if(session('active_role'))
<span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
@endif
<div class="relative">
<details>
<summary class="list-none flex items-center gap-2 cursor-pointer">
<div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
{{ strtoupper(substr(auth()->user()->name ?? 'U',0,1)) }}
</div>
<span class="text-sm">{{ auth()->user()->name }}</span>
</summary>
<div class="absolute right-0 mt-2 bg-white border rounded shadow min-w-[200px]">
<a class="block px-3 py-2 hover:bg-gray-50" href="{{ route('auth.role.select') }}">Cambia ruolo</a>
<a class="block px-3 py-2 hover:bg-gray-50" href="#">Profilo</a>
<form method="POST" action="{{ route('logout') }}" class="border-t">
@csrf
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">Logout</button>
</form>
</div>
</details>
</div>
</div>
</header> <div class="flex">
{{-- SIDEBAR: L2/L3/L4 --}}
<aside class="w-72 bg-white border-r min-h-[calc(100vh-3.5rem)] p-4">
@if(!empty($sidebarTree))
@foreach($sidebarTree as $l2)
<div class="mb-3">
<div class="text-sm font-semibold mb-1">{{ $l2['label'] }}</div>
@if(!empty($l2['children']))
<ul class="ml-3 space-y-1">
@foreach($l2['children'] as $l3)
<li>
<a class="block text-sm {{ request()->routeIs(($l3['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
href="{{ $l3['route_name'] ? route($l3['route_name']) : ($l3['url'] ?? '#') }}">
{{ $l3['label'] }}
</a>
@if(!empty($l3['children']))
<ul class="ml-4 mt-1 space-y-1">
@foreach($l3['children'] as $l4)
<li>
<a class="block text-sm {{ request()->routeIs(($l4['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
href="{{ $l4['route_name'] ? route($l4['route_name']) : ($l4['url'] ?? '#') }}">
{{ $l4['label'] }}
</a>
</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
{{-- MAIN: breadcrumb + titolo + body --}}
<main class="flex-1 p-6">
<div class="mb-4">
<nav class="text-sm text-gray-500">
@if(!empty($active['trail']))
@foreach($active['trail'] as $i => $crumb)
@if($i>0) <span class="mx-1">/</span> @endif
@if($crumb['route_name'])
<a class="hover:underline" href="{{ route($crumb['route_name']) }}">{{ $crumb['label'] }}</a>
@else
<span>{{ $crumb['label'] }}</span>
@endif
@endforeach
@endif
</nav>
<h1 class="text-xl font-semibold mt-1">@yield('title','Modulo')</h1>
</div>
@yield('content')
</main>
</div>
</body>
</html>
`---
3) Service: branch attivo, sidebar e topbar
3.1 Metodi aggiuntivi in
MenuService
`php
// App/Services/MenuService.php (estratti/aggiunte)
public function currentBranchByRoute(string $menuName = 'admin_main'): array
{
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return ['trail'=>[], 'top'=>null];
$items = $menu->items()->get();
$byId = $items->keyBy('id');
$current = $items->first(function($i){ return $i->route_name && request()->routeIs($i->route_name.'*'); });
if (!$current) return ['trail'=>[], 'top'=>null]; $trail = [];
while ($current) {
$trail[] = ['label'=>$current->label,'route_name'=>$current->route_name,'id'=>$current->id,'parent_id'=>$current->parent_id];
$current = $current->parent_id ? $byId->get($current->parent_id) : null;
}
$trail = array_reverse($trail);
$top = $trail[0] ?? null;
return ['trail'=>$trail, 'top'=>$top];
}
public function childrenForTopLevel(?array $top): array
{
if (!$top) return [];
$menu = Menu::where('name','admin_main')->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$build = function($parentId) use (&$build,$items) {
return ($items[$parentId] ?? collect())->map(fn($i)=>
['id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'children'=>$build($i->id)->values()->all()]
);
};
return $build($top['id'] ?? null)->values()->all();
}
`> Il filtraggio per ruolo/permessi è già presente nel metodo principale che costruisce L1. Per coerenza, puoi riapplicarlo anche alla sidebar (stessa closure
filter).---
4) Rotte & accessi
4.1 RouteServiceProvider
`php
// app/Providers/RouteServiceProvider.php (estratto)
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(function () {
require base_path('routes/auth.php');
require base_path('routes/admin.php');
});
}
`4.2 Rotte auth (login esterno + role select)
`php
// routes/auth.php (estratto)
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login'])->name('login.post');
Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
`4.3 Rotte area Admin (contenitore unico)
`php
// routes/admin.php (estratto)
Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])->name('dashboard');
Route::prefix('i40')->name('i40.')->group(function() {
Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])->name('machines.connected');
});
Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
Route::view('/', 'admin.systems.index')->name('home');
Route::prefix('menu')->name('menu.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
});
});
});
`4.4 Sicurezza & middleware
`php
// app/Http/Kernel.php (estratto)
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'active.role' => \App\Http\Middleware\EnsureActiveRole::class,
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'roles_or_permissions' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
`
`php
// app/Providers/AuthServiceProvider.php (estratto)
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
`---
5) Esempio modulo (I4.0 → Macchine → Elenco collegate)
`blade
{{-- resources/views/admin/i40/machines/connected.blade.php --}}
@extends('layouts.admin')
@section('title','Elenco macchine collegate')
@section('content')
<div class="bg-white border rounded shadow p-4">
<div class="mb-3 text-sm text-gray-500">Tabella in tempo reale (placeholder)</div>
<table class="min-w-full">
<thead>
<tr class="text-left text-sm text-gray-600">
<th class="p-2">Macchina</th>
<th class="p-2">Protocollo</th>
<th class="p-2">Stato</th>
<th class="p-2">Last seen</th>
</tr>
</thead>
<tbody>
<tr class="border-t">
<td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td>
</tr>
</tbody>
</table>
</div>
@endsection
`---
6) Seed essenziale (menù L1 + Servizio + Configurazione menù)
`php
$admin = \App\Models\Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard'
],[
'route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1
]);
$ind40 = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0'
],[ 'icon'=>'lucide-cpu','order_index'=>2 ]);
$report = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report'
],[ 'order_index'=>1 ]);
$macchine = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine'
],[ 'order_index'=>1 ]);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate'
],[
'route_name'=>'admin.i40.machines.connected',
'order_index'=>1,
'required_roles'=>json_encode(['admin','maintenance','super-admin']) // aggiungi 'operator' se vuoi
]);
$service = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
'icon'=>'lucide-wrench','order_index'=>100,
'required_roles'=>json_encode(['super-admin'])
]);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
'route_name'=>'admin.systems.menu.index',
'order_index'=>1,
'required_roles'=>json_encode(['super-admin'])
]);
``---
7) Criteri di accettazione
---
8) Note operative
Analisi Codice
Blocco 1
app/Http/Controllers/Admin/
Dashboard/DashboardController.php
I40/HomeController.php
I40/Machines/MachinesController.php
Systems/ # “Servizio” (solo super-admin)
Menu/MenuController.php
Users/UsersController.php (placeholder)
Roles/RolesController.php (placeholder)
app/Services/MenuService.php
app/Http/Middleware/EnsureActiveRole.php
app/Policies/MenuPolicy.php (opzionale)
app/Models/Menu.php
app/Models/MenuItem.php
resources/views/layouts/admin.blade.php
resources/views/layouts/auth.blade.php # login esterno (1/3–2/3)
resources/views/auth/{login,select-role,forgot,reset}.blade.php
resources/views/admin/dashboard/index.blade.php
resources/views/admin/i40/home.blade.php
resources/views/admin/i40/machines/connected.blade.php
resources/views/admin/systems/index.blade.php
resources/views/admin/systems/menu/{index,create,edit}.blade.php
routes/auth.php
routes/admin.php
Blocco 2 blade
{{-- resources/views/layouts/auth.blade.php --}}
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>@yield('title','Accedi | sartUP')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
<div class="mx-auto max-w-6xl min-h-screen grid grid-cols-3">
{{-- Colonna sinistra 1/3: branding --}}
<aside class="col-span-1 bg-white border-r p-8 flex flex-col justify-center">
<div class="flex items-center gap-3 mb-6">
<img src="/images/logo.png" alt="sartUP" class="h-12 w-12 rounded-full object-cover">
<div>
<h1 class="text-xl font-semibold">sartUP</h1>
<p class="text-sm text-gray-500">Gestionale modulare per confezione</p>
</div>
</div>
<div class="text-gray-600 text-sm leading-relaxed">
<p>Accedi per gestire modelli, tempi & metodi, integrazioni Industria 4.0 e reportistica.</p>
</div>
{{-- <img src="/images/login-side.jpg" class="mt-8 rounded-xl shadow" alt=""> --}}
</aside>
{{-- Colonna destra 2/3: form --}}
<main class="col-span-2 flex items-center justify-center p-8">
<div class="w-full max-w-md bg-white rounded-xl shadow p-6">
@yield('content')
</div>
</main>
</div>
</body>
</html>
Blocco 3 blade
{{-- resources/views/auth/login.blade.php --}}
@extends('layouts.auth')
@section('title','Accedi')
@section('content')
<h2 class="text-lg font-semibold mb-4">Accedi</h2>
<form method="POST" action="{{ route('login.post') }}" class="space-y-4">
@csrf
<div>
<label class="block text-sm mb-1">Email</label>
<input type="email" name="email" value="{{ old('email') }}" required autofocus class="w-full border rounded p-2">
@error('email') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div>
<label class="block text-sm mb-1">Password</label>
<input type="password" name="password" required class="w-full border rounded p-2">
@error('password') <p class="text-red-600 text-sm mt-1">{{ $message }}</p> @enderror
</div>
<div class="flex items-center justify-between">
<label class="inline-flex items-center gap-2 text-sm">
<input type="checkbox" name="remember" class="rounded"> Ricordami
</label>
<a class="text-sm underline" href="{{ route('password.request') }}">Hai dimenticato la password?</a>
</div>
<button class="w-full py-2 rounded bg-blue-600 text-white font-medium">Entra</button>
</form>
@endsection
Blocco 4 blade
{{-- resources/views/layouts/admin.blade.php --}}
@php
/** @var \App\Services\MenuService $ms */
$ms = app(\App\Services\MenuService::class);
$menuL1 = $ms->forUserMenu('admin_main', auth()->user()); // topbar
$active = $ms->currentBranchByRoute('admin_main'); // item corrente + ancestors
$sidebarTree = $ms->childrenForTopLevel($active['top'] ?? $menuL1[0] ?? null); // L2/L3/L4
@endphp
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>@yield('title','sartUP Admin')</title>
@vite(['resources/css/app.css','resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-50">
{{-- TOPBAR --}}
<header class="h-14 shadow flex items-center px-4 bg-white">
<div class="flex items-center gap-3 mr-6">
<img src="/images/logo.png" class="h-8 w-8 rounded-full object-cover" alt="logo">
</div>
<nav class="flex gap-4">
@foreach($menuL1 as $item)
<a href="{{ $item['route_name'] ? route($item['route_name']) : ($item['url'] ?? '#') }}"
class="font-medium {{ request()->routeIs(($item['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}">
{{ $item['label'] }}
</a>
@endforeach
@role('super-admin')
<a href="{{ route('admin.systems.menu.index') }}"
class="font-medium {{ request()->is('admin/systems*') ? 'text-blue-600' : '' }}">Servizio</a>
@endrole
</nav>
<div class="ml-auto flex items-center gap-3">
@if(session('active_role'))
<span class="text-xs px-2 py-1 bg-gray-200 rounded">Ruolo: {{ session('active_role') }}</span>
@endif
<div class="relative">
<details>
<summary class="list-none flex items-center gap-2 cursor-pointer">
<div class="h-8 w-8 rounded-full bg-gray-200 flex items-center justify-center">
{{ strtoupper(substr(auth()->user()->name ?? 'U',0,1)) }}
</div>
<span class="text-sm">{{ auth()->user()->name }}</span>
</summary>
<div class="absolute right-0 mt-2 bg-white border rounded shadow min-w-[200px]">
<a class="block px-3 py-2 hover:bg-gray-50" href="{{ route('auth.role.select') }}">Cambia ruolo</a>
<a class="block px-3 py-2 hover:bg-gray-50" href="#">Profilo</a>
<form method="POST" action="{{ route('logout') }}" class="border-t">
@csrf
<button class="w-full text-left px-3 py-2 hover:bg-gray-50">Logout</button>
</form>
</div>
</details>
</div>
</div>
</header>
<div class="flex">
{{-- SIDEBAR: L2/L3/L4 --}}
<aside class="w-72 bg-white border-r min-h-[calc(100vh-3.5rem)] p-4">
@if(!empty($sidebarTree))
@foreach($sidebarTree as $l2)
<div class="mb-3">
<div class="text-sm font-semibold mb-1">{{ $l2['label'] }}</div>
@if(!empty($l2['children']))
<ul class="ml-3 space-y-1">
@foreach($l2['children'] as $l3)
<li>
<a class="block text-sm {{ request()->routeIs(($l3['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
href="{{ $l3['route_name'] ? route($l3['route_name']) : ($l3['url'] ?? '#') }}">
{{ $l3['label'] }}
</a>
@if(!empty($l3['children']))
<ul class="ml-4 mt-1 space-y-1">
@foreach($l3['children'] as $l4)
<li>
<a class="block text-sm {{ request()->routeIs(($l4['route_name'] ?? '').'*') ? 'text-blue-600' : '' }}"
href="{{ $l4['route_name'] ? route($l4['route_name']) : ($l4['url'] ?? '#') }}">
{{ $l4['label'] }}
</a>
</li>
@endforeach
</ul>
@endif
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
@endif
</aside>
{{-- MAIN: breadcrumb + titolo + body --}}
<main class="flex-1 p-6">
<div class="mb-4">
<nav class="text-sm text-gray-500">
@if(!empty($active['trail']))
@foreach($active['trail'] as $i => $crumb)
@if($i>0) <span class="mx-1">/</span> @endif
@if($crumb['route_name'])
<a class="hover:underline" href="{{ route($crumb['route_name']) }}">{{ $crumb['label'] }}</a>
@else
<span>{{ $crumb['label'] }}</span>
@endif
@endforeach
@endif
</nav>
<h1 class="text-xl font-semibold mt-1">@yield('title','Modulo')</h1>
</div>
@yield('content')
</main>
</div>
</body>
</html>
Blocco 5 php
// App/Services/MenuService.php (estratti/aggiunte)
public function currentBranchByRoute(string $menuName = 'admin_main'): array
{
$menu = Menu::where('name',$menuName)->first();
if (!$menu) return ['trail'=>[], 'top'=>null];
$items = $menu->items()->get();
$byId = $items->keyBy('id');
$current = $items->first(function($i){ return $i->route_name && request()->routeIs($i->route_name.'*'); });
if (!$current) return ['trail'=>[], 'top'=>null];
$trail = [];
while ($current) {
$trail[] = ['label'=>$current->label,'route_name'=>$current->route_name,'id'=>$current->id,'parent_id'=>$current->parent_id];
$current = $current->parent_id ? $byId->get($current->parent_id) : null;
}
$trail = array_reverse($trail);
$top = $trail[0] ?? null;
return ['trail'=>$trail, 'top'=>$top];
}
public function childrenForTopLevel(?array $top): array
{
if (!$top) return [];
$menu = Menu::where('name','admin_main')->first();
if (!$menu) return [];
$items = $menu->items()->orderBy('order_index')->get()->groupBy('parent_id');
$build = function($parentId) use (&$build,$items) {
return ($items[$parentId] ?? collect())->map(fn($i)=>
['id'=>$i->id,'label'=>$i->label,'route_name'=>$i->route_name,'url'=>$i->url,'children'=>$build($i->id)->values()->all()]
);
};
return $build($top['id'] ?? null)->values()->all();
}
Blocco 6 php
// app/Providers/RouteServiceProvider.php (estratto)
public function boot(): void
{
parent::boot();
Route::middleware('web')->group(function () {
require base_path('routes/auth.php');
require base_path('routes/admin.php');
});
}
Blocco 7 php
// routes/auth.php (estratto)
Route::middleware('guest')->group(function () {
Route::get('/login', [LoginController::class, 'showLoginForm'])->name('login');
Route::post('/login', [LoginController::class, 'login'])->name('login.post');
Route::get('/password/forgot', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('password.request');
Route::post('/password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('password.email');
Route::get('/password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update');
});
Route::post('/logout', [LoginController::class, 'logout'])->name('logout')->middleware('auth');
Route::middleware('auth')->group(function () {
Route::get('/auth/select-role', [RoleSelectorController::class,'show'])->name('auth.role.select');
Route::post('/auth/set-role', [RoleSelectorController::class,'set'])->name('auth.role.set');
});
Blocco 8 php
// routes/admin.php (estratto)
Route::middleware(['auth','active.role'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Dashboard\DashboardController::class,'index'])->name('dashboard');
Route::prefix('i40')->name('i40.')->group(function() {
Route::get('/', [\App\Http\Controllers\Admin\I40\HomeController::class,'index'])->name('home');
Route::get('/machines/connected', [\App\Http\Controllers\Admin\I40\Machines\MachinesController::class,'connected'])->name('machines.connected');
});
Route::prefix('systems')->name('systems.')->middleware('role:super-admin')->group(function () {
Route::view('/', 'admin.systems.index')->name('home');
Route::prefix('menu')->name('menu.')->group(function () {
Route::get('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'index'])->name('index');
Route::get('/create', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'create'])->name('create');
Route::post('/', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'store'])->name('store');
Route::get('/{item}/edit', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'edit'])->name('edit');
Route::put('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'update'])->name('update');
Route::delete('/{item}', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'destroy'])->name('destroy');
Route::post('/reorder', [\App\Http\Controllers\Admin\Systems\Menu\MenuController::class,'reorder'])->name('reorder');
});
});
});
Blocco 9 php
// app/Http/Kernel.php (estratto)
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'active.role' => \App\Http\Middleware\EnsureActiveRole::class,
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'roles_or_permissions' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
Blocco 10 php
// app/Providers/AuthServiceProvider.php (estratto)
Gate::before(function ($user, $ability) {
return $user->hasRole('super-admin') ? true : null;
});
Blocco 11 blade
{{-- resources/views/admin/i40/machines/connected.blade.php --}}
@extends('layouts.admin')
@section('title','Elenco macchine collegate')
@section('content')
<div class="bg-white border rounded shadow p-4">
<div class="mb-3 text-sm text-gray-500">Tabella in tempo reale (placeholder)</div>
<table class="min-w-full">
<thead>
<tr class="text-left text-sm text-gray-600">
<th class="p-2">Macchina</th>
<th class="p-2">Protocollo</th>
<th class="p-2">Stato</th>
<th class="p-2">Last seen</th>
</tr>
</thead>
<tbody>
<tr class="border-t">
<td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td><td class="p-2">—</td>
</tr>
</tbody>
</table>
</div>
@endsection
Blocco 12 php
$admin = \App\Models\Menu::firstOrCreate(['name'=>'admin_main'], ['description'=>'Menu principale admin']);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Dashboard'
],[
'route_name'=>'admin.dashboard','icon'=>'lucide-home','order_index'=>1
]);
$ind40 = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Industria 4.0'
],[ 'icon'=>'lucide-cpu','order_index'=>2 ]);
$report = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$ind40->id,'label'=>'Report'
],[ 'order_index'=>1 ]);
$macchine = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$report->id,'label'=>'Macchine'
],[ 'order_index'=>1 ]);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$macchine->id,'label'=>'Elenco macchine collegate'
],[
'route_name'=>'admin.i40.machines.connected',
'order_index'=>1,
'required_roles'=>json_encode(['admin','maintenance','super-admin']) // aggiungi 'operator' se vuoi
]);
$service = \App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>null,'label'=>'Servizio'
],[
'icon'=>'lucide-wrench','order_index'=>100,
'required_roles'=>json_encode(['super-admin'])
]);
\App\Models\MenuItem::firstOrCreate([
'menu_id'=>$admin->id,'parent_id'=>$service->id,'label'=>'Configurazione menù'
],[
'route_name'=>'admin.systems.menu.index',
'order_index'=>1,
'required_roles'=>json_encode(['super-admin'])
]);